/*
Copyright 2019 The KubeSphere Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package workspacetemplate

import (
	"bytes"
	"context"
	"fmt"
	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/record"
	iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
	tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
	tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2"
	typesv1beta1 "kubesphere.io/kubesphere/pkg/apis/types/v1beta1"
	"kubesphere.io/kubesphere/pkg/constants"
	controllerutils "kubesphere.io/kubesphere/pkg/controller/utils/controller"
	"reflect"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	controllerName = "workspacetemplate-controller"
)

// Reconciler reconciles a WorkspaceRoleBinding object
type Reconciler struct {
	client.Client
	Logger                  logr.Logger
	Recorder                record.EventRecorder
	MaxConcurrentReconciles int
	MultiClusterEnabled     bool
}

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
	if r.Client == nil {
		r.Client = mgr.GetClient()
	}
	if r.Logger == nil {
		r.Logger = ctrl.Log.WithName("controllers").WithName(controllerName)
	}
	if r.Recorder == nil {
		r.Recorder = mgr.GetEventRecorderFor(controllerName)
	}
	if r.MaxConcurrentReconciles <= 0 {
		r.MaxConcurrentReconciles = 1
	}
	return ctrl.NewControllerManagedBy(mgr).
		Named(controllerName).
		WithOptions(controller.Options{
			MaxConcurrentReconciles: r.MaxConcurrentReconciles,
		}).
		For(&tenantv1alpha2.WorkspaceTemplate{}).
		Complete(r)
}

// +kubebuilder:rbac:groups=iam.kubesphere.io,resources=workspacerolebindings,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=types.kubefed.io,resources=federatedworkspacerolebindings,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=tenant.kubesphere.io,resources=workspaces,verbs=get;list;watch;
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
	logger := r.Logger.WithValues("workspacetemplate", req.NamespacedName)
	rootCtx := context.Background()
	workspaceTemplate := &tenantv1alpha2.WorkspaceTemplate{}
	if err := r.Get(rootCtx, req.NamespacedName, workspaceTemplate); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	if r.MultiClusterEnabled {
		if err := r.multiClusterSync(rootCtx, logger, workspaceTemplate); err != nil {
			return ctrl.Result{}, err
		}
	} else {
		if err := r.singleClusterSync(rootCtx, logger, workspaceTemplate); err != nil {
			return ctrl.Result{}, err
		}
	}
	if err := r.initWorkspaceRoles(rootCtx, logger, workspaceTemplate); err != nil {
		return ctrl.Result{}, err
	}
	if err := r.initManagerRoleBinding(rootCtx, logger, workspaceTemplate); err != nil {
		return ctrl.Result{}, err
	}
	r.Recorder.Event(workspaceTemplate, corev1.EventTypeNormal, controllerutils.SuccessSynced, controllerutils.MessageResourceSynced)
	return ctrl.Result{}, nil
}

func (r *Reconciler) singleClusterSync(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error {
	workspace := &tenantv1alpha1.Workspace{}
	if err := r.Get(ctx, types.NamespacedName{Name: workspaceTemplate.Name}, workspace); err != nil {
		if errors.IsNotFound(err) {
			if workspace, err := newWorkspace(workspaceTemplate); err != nil {
				logger.Error(err, "generate workspace failed")
				return err
			} else {
				if err := r.Create(ctx, workspace); err != nil {
					logger.Error(err, "create workspace failed")
					return err
				}
			}
		}
		logger.Error(err, "get workspace failed")
		return err
	}
	if !reflect.DeepEqual(workspace.Spec, workspaceTemplate.Spec.Template.Spec) ||
		!reflect.DeepEqual(workspace.Labels, workspaceTemplate.Spec.Template.Labels) {

		workspace = workspace.DeepCopy()
		workspace.Spec = workspaceTemplate.Spec.Template.Spec
		workspace.Labels = workspaceTemplate.Spec.Template.Labels

		if err := r.Update(ctx, workspace); err != nil {
			logger.Error(err, "update workspace failed")
			return err
		}
	}
	return nil
}

func (r *Reconciler) multiClusterSync(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error {
	if err := r.ensureNotControlledByKubefed(ctx, logger, workspaceTemplate); err != nil {
		return err
	}
	federatedWorkspace := &typesv1beta1.FederatedWorkspace{}
	if err := r.Client.Get(ctx, types.NamespacedName{Name: workspaceTemplate.Name}, federatedWorkspace); err != nil {
		if errors.IsNotFound(err) {
			if federatedWorkspace, err := newFederatedWorkspace(workspaceTemplate); err != nil {
				logger.Error(err, "generate federated workspace failed")
				return err
			} else {
				if err := r.Create(ctx, federatedWorkspace); err != nil {
					logger.Error(err, "create federated workspace failed")
					return err
				}
			}
		}
		logger.Error(err, "get federated workspace failed")
		return err
	}

	if !reflect.DeepEqual(federatedWorkspace.Spec, workspaceTemplate.Spec) ||
		!reflect.DeepEqual(federatedWorkspace.Labels, workspaceTemplate.Labels) {

		federatedWorkspace.Spec = workspaceTemplate.Spec
		federatedWorkspace.Labels = workspaceTemplate.Labels

		if err := r.Update(ctx, federatedWorkspace); err != nil {
			logger.Error(err, "update federated workspace failed")
			return err
		}
	}

	return nil
}

func newFederatedWorkspace(template *tenantv1alpha2.WorkspaceTemplate) (*typesv1beta1.FederatedWorkspace, error) {
	federatedWorkspace := &typesv1beta1.FederatedWorkspace{
		TypeMeta: metav1.TypeMeta{
			Kind:       typesv1beta1.FederatedWorkspaceRoleKind,
			APIVersion: typesv1beta1.SchemeGroupVersion.String(),
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:   template.Name,
			Labels: template.Labels,
		},
		Spec: template.Spec,
	}
	if err := controllerutil.SetControllerReference(template, federatedWorkspace, scheme.Scheme); err != nil {
		return nil, err
	}
	return federatedWorkspace, nil
}

func newWorkspace(template *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha1.Workspace, error) {
	workspace := &tenantv1alpha1.Workspace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   template.Name,
			Labels: template.Spec.Template.Labels,
		},
		Spec: template.Spec.Template.Spec,
	}
	if err := controllerutil.SetControllerReference(template, workspace, scheme.Scheme); err != nil {
		return nil, err
	}
	return workspace, nil
}

func (r *Reconciler) ensureNotControlledByKubefed(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error {
	if workspaceTemplate.Labels[constants.KubefedManagedLabel] != "false" {
		if workspaceTemplate.Labels == nil {
			workspaceTemplate.Labels = make(map[string]string)
		}
		workspaceTemplate = workspaceTemplate.DeepCopy()
		workspaceTemplate.Labels[constants.KubefedManagedLabel] = "false"
		logger.V(4).Info("update kubefed managed label")
		if err := r.Update(ctx, workspaceTemplate); err != nil {
			logger.Error(err, "update kubefed managed label failed")
			return err
		}
	}
	return nil
}

func (r *Reconciler) initWorkspaceRoles(ctx context.Context, logger logr.Logger, workspace *tenantv1alpha2.WorkspaceTemplate) error {
	var templates iamv1alpha2.RoleBaseList
	if err := r.List(ctx, &templates); err != nil {
		logger.Error(err, "list role base failed")
		return err
	}
	for _, template := range templates.Items {
		var expected iamv1alpha2.WorkspaceRole
		if err := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(template.Role.Raw), 1024).Decode(&expected); err == nil && expected.Kind == iamv1alpha2.ResourceKindWorkspaceRole {
			expected.Name = fmt.Sprintf("%s-%s", workspace.Name, expected.Name)
			if expected.Labels == nil {
				expected.Labels = make(map[string]string)
			}
			expected.Labels[tenantv1alpha1.WorkspaceLabel] = workspace.Name
			var existed iamv1alpha2.WorkspaceRole
			if err := r.Get(ctx, types.NamespacedName{Name: expected.Name}, &existed); err != nil {
				if errors.IsNotFound(err) {
					logger.V(4).Info("create workspace role", "workspacerole", expected.Name)
					if err := r.Create(ctx, &expected); err != nil {
						logger.Error(err, "create workspace role failed")
						return err
					}
					continue
				} else {
					logger.Error(err, "get workspace role failed")
					return err
				}
			}
			if !reflect.DeepEqual(expected.Labels, existed.Labels) ||
				!reflect.DeepEqual(expected.Annotations, existed.Annotations) ||
				!reflect.DeepEqual(expected.Rules, existed.Rules) {
				updated := existed.DeepCopy()
				updated.Labels = expected.Labels
				updated.Annotations = expected.Annotations
				updated.Rules = expected.Rules
				logger.V(4).Info("update workspace role", "workspacerole", updated.Name)
				if err := r.Update(ctx, updated); err != nil {
					logger.Error(err, "update workspace role failed")
					return err
				}
			}
		} else if err != nil {
			logger.Error(fmt.Errorf("invalid role base found"), "init workspace roles failed", "name", template.Name)
		}
	}
	return nil
}

func (r *Reconciler) initManagerRoleBinding(ctx context.Context, logger logr.Logger, workspace *tenantv1alpha2.WorkspaceTemplate) error {
	manager := workspace.Spec.Template.Spec.Manager
	if manager == "" {
		return nil
	}

	var user iamv1alpha2.User
	if err := r.Get(ctx, types.NamespacedName{Name: manager}, &user); err != nil {
		return client.IgnoreNotFound(err)
	}

	// skip if user has been deleted
	if !user.DeletionTimestamp.IsZero() {
		return nil
	}

	workspaceAdminRoleName := fmt.Sprintf("%s-admin", workspace.Name)
	managerRoleBinding := &iamv1alpha2.WorkspaceRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: workspaceAdminRoleName,
		},
	}

	if _, err := ctrl.CreateOrUpdate(ctx, r.Client, managerRoleBinding, workspaceRoleBindingChanger(managerRoleBinding, workspace.Name, manager, workspaceAdminRoleName)); err != nil {
		logger.Error(err, "create workspace manager role binding failed")
		return err
	}

	return nil
}

func workspaceRoleBindingChanger(workspaceRoleBinding *iamv1alpha2.WorkspaceRoleBinding, workspace, username, workspaceRoleName string) controllerutil.MutateFn {
	return func() error {
		workspaceRoleBinding.Labels = map[string]string{
			tenantv1alpha1.WorkspaceLabel:  workspace,
			iamv1alpha2.UserReferenceLabel: username,
		}

		workspaceRoleBinding.RoleRef = rbacv1.RoleRef{
			APIGroup: iamv1alpha2.SchemeGroupVersion.Group,
			Kind:     iamv1alpha2.ResourceKindWorkspaceRole,
			Name:     workspaceRoleName,
		}

		workspaceRoleBinding.Subjects = []rbacv1.Subject{
			{
				Name:     username,
				Kind:     rbacv1.UserKind,
				APIGroup: rbacv1.GroupName,
			},
		}
		return nil
	}
}
